/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file pyhp_mysql.c
 * pyhp database mysql support
 */

#include "mysql.h"
#include "mysqld_error.h"

#include "pyhp_mysql.h"

#define HOST_ASSIGN_LEN 5
#define DBNAME_ASSIGN_LEN 7
#define PORT_ASSIGN_LEN 5

typedef struct pyhp_mysql_result
{
    PyObject_HEAD
    MYSQL *connection;
    MYSQL_RES *result;
} PyHP_MySQL_Result;


/**
 * Allocator for MySQL Query Result.
 * Must NOT be called from the python layer, the objects is not
 * thinked to be creable from python. Only mod_pyhp can create it.
 */
static PyHP_MySQL_Result *PyHP_MySQL_Result_new(PyTypeObject *type,
                                                MYSQL *connection)
{
    PyHP_MySQL_Result *res = (PyHP_MySQL_Result *)type->tp_alloc(type, 0);
    if (res) {
        res->connection = connection;
        res->result = NULL;
    }
    return res;
}

/**
 * MySQL Query Result deallocator
 */
static void PyHP_MySQL_Result_dealloc(PyHP_MySQL_Result *res)
{
    if (res->result) mysql_free_result(res->result);
    res->ob_type->tp_free((PyObject*)res);
}

/**
 * Simples toString converter for mysql query result,
 * will just report that it is a Query result for MySQL
 */
static PyObject *PyHP_MySQL_Result_str(PyHP_MySQL_Result *self)
{
    return PyString_FromString("PyHP MySQL Query Result");
}

static PyObject *PyHP_MySQL_Result_iter(PyHP_MySQL_Result *self)
{
    Py_INCREF(self);
    return(PyObject*)self;
}

/**
 * Converts a DATE/DATETIME/TIME column to the corrispective
 * datetime.date/time/datetime python object
 *
 * @param str The date, time or datetime string in "%Y-%m-%d %H:%M:%S" format
 * @param type The type of the column: MYSQL_TYPE_DATE/DATETIME/TIME
 *
 * @return The corrispective datetime object or None
 */
static PyObject *datetime_from_string(char const *str, int type)
{
    PyObject *datetime;
    PyObject *time_tuple;
    PyObject *args = NULL;

    PyObject *date_module = PyImport_ImportModule("datetime");
    PyObject *date_mdict = PyModule_GetDict(date_module);
    PyObject *time_module = PyImport_ImportModule("time");
    PyObject *time_mdict = PyModule_GetDict(time_module);

    PyObject *time = PyDict_GetItemString(time_mdict, "strptime");

    if (type == MYSQL_TYPE_DATETIME) {
        datetime = PyDict_GetItemString(date_mdict, "datetime");
        time_tuple = PyObject_CallFunction(time, "(ss)", str, "%Y-%m-%d %H:%M:%S");
        args = PyTuple_New(6);

        PyTuple_SetItem(args, 0, PyObject_GetAttrString(time_tuple, "tm_year"));
        PyTuple_SetItem(args, 1, PyObject_GetAttrString(time_tuple, "tm_mon"));
        PyTuple_SetItem(args, 2, PyObject_GetAttrString(time_tuple, "tm_mday"));
        PyTuple_SetItem(args, 3, PyObject_GetAttrString(time_tuple, "tm_hour"));
        PyTuple_SetItem(args, 4, PyObject_GetAttrString(time_tuple, "tm_min"));
        PyTuple_SetItem(args, 5, PyObject_GetAttrString(time_tuple, "tm_sec"));

        Py_XDECREF(time_tuple);
    }
    else if ( (type == MYSQL_TYPE_DATE) || (type == MYSQL_TYPE_NEWDATE) ) {
        datetime = PyDict_GetItemString(date_mdict, "date");
        time_tuple = PyObject_CallFunction(time, "(ss)", str, "%Y-%m-%d");
        args = PyTuple_New(3);

        PyTuple_SetItem(args, 0, PyObject_GetAttrString(time_tuple, "tm_year"));
        PyTuple_SetItem(args, 1, PyObject_GetAttrString(time_tuple, "tm_mon"));
        PyTuple_SetItem(args, 2, PyObject_GetAttrString(time_tuple, "tm_mday"));

        Py_XDECREF(time_tuple);
    }
    else if (type == MYSQL_TYPE_TIME) {
        datetime = PyDict_GetItemString(date_mdict, "time");
        time_tuple = PyObject_CallFunction(time, "(ss)", str, "%H:%M:%S");
        args = PyTuple_New(3);

        PyTuple_SetItem(args, 0, PyObject_GetAttrString(time_tuple, "tm_hour"));
        PyTuple_SetItem(args, 1, PyObject_GetAttrString(time_tuple, "tm_min"));
        PyTuple_SetItem(args, 2, PyObject_GetAttrString(time_tuple, "tm_sec"));

        Py_XDECREF(time_tuple);
    }
    else {
        Py_DECREF(time_module);
        Py_DECREF(date_module);
        Py_INCREF(Py_None);
        return Py_None;
    }


    PyObject *instance = PyObject_CallObject(datetime, args);
    Py_XDECREF(args);


    Py_DECREF(time_module);
    Py_DECREF(date_module);

    return instance;
}

/**
 * Converts a MySQL row value to the corrispective python object and adds
 * it to a dictionary with the column name as key. If there isn't any
 * valid conversion to a python object the value will be rappresented as a
 * string.
 *
 * @param pyrow The dictionary where to add the value
 * @param row The MYSQL row containing the value
 * @param fiels The columns of the query from which the row has been retrieved
 * @param lenghts The lenghts of the values contained inside the row
 * @param i The index of the value we want to convert inside the row
 *
 * @return always 0
 */
static int pyhp_mysql_add_field(PyObject *pyrow, MYSQL_ROW row, MYSQL_FIELD *fields, unsigned long *lengths, int i)
{
    PyObject *tmp = NULL;

    switch(fields[i].type) {
        case MYSQL_TYPE_TINY:
        case MYSQL_TYPE_SHORT:
        case MYSQL_TYPE_LONG:
        case MYSQL_TYPE_LONGLONG:
        case MYSQL_TYPE_INT24:
        case MYSQL_TYPE_YEAR:
            // integer
            tmp = PyLong_FromString(row[i], NULL, 0);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            break;
        case MYSQL_TYPE_FLOAT:
        case MYSQL_TYPE_DOUBLE:
        case MYSQL_TYPE_DECIMAL:
        case MYSQL_TYPE_NEWDECIMAL:
        {
            // float
            PyObject *s = PyString_FromString(row[i]);
            tmp = PyFloat_FromString(s, NULL);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            Py_XDECREF(s);
        }
            break;
        case MYSQL_TYPE_BIT:
            // boolean
            if (row[i][0])
                tmp = Py_True;
            else
                tmp = Py_False;
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            break;
        case MYSQL_TYPE_BLOB:
        case MYSQL_TYPE_LONG_BLOB:
        case MYSQL_TYPE_MEDIUM_BLOB:
        case MYSQL_TYPE_TINY_BLOB:
            // buffer
            tmp = PyString_FromStringAndSize(row[i], lengths[i]);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            break;
        case MYSQL_TYPE_STRING:
        case MYSQL_TYPE_VAR_STRING:
        case MYSQL_TYPE_VARCHAR:
            // string
            tmp = PyString_FromString(row[i]);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            break;
        case MYSQL_TYPE_TIME:
        case MYSQL_TYPE_DATE:
        case MYSQL_TYPE_NEWDATE:
        case MYSQL_TYPE_DATETIME:
            tmp = datetime_from_string(row[i], fields[i].type);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            break;
        default:
            tmp = PyString_FromString(row[i]);
            PyDict_SetItemString(pyrow, fields[i].name, tmp);
            Py_XDECREF(tmp);
            break;
    }

    return 0;
}

/**
 * Python member function of the Query Result object to fetch all
 * the rows of the query.
 *
 * @return A list where each entry is a dictionary containing the values of
 *         the query. Each key of the dictionary will be the column name.
 */
PyObject *pyhp_mysql_fetch_all(PyHP_MySQL_Result *self, PyObject *args)
{
    PyObject *res = PyList_New(0);
    MYSQL_ROW row;
    MYSQL_FIELD *fields;
    unsigned int num_fields;
    unsigned int i;

    if (!self->result) {
        self->result = mysql_store_result(self->connection);
    }

    if (!self->result) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    num_fields = mysql_num_fields(self->result);
    fields = mysql_fetch_fields(self->result);
    while ((row = mysql_fetch_row(self->result))) {
        unsigned long *lengths = mysql_fetch_lengths(self->result);
        PyObject * pyrow = PyDict_New();

        PyList_Append(res, pyrow);
        Py_XDECREF(pyrow);

        for(i = 0; i < num_fields; i++) {
            if (!row[i]) {
                PyDict_SetItemString(pyrow, fields[i].name, Py_None);
                continue;
            }

            pyhp_mysql_add_field(pyrow, row, fields, lengths, i);
        }
    }

    return res;
}

/**
 * Python member function of the Query Result object to fetch only a
 * single row of the query. This will use a cursor allocating memory
 * only for the current row.
 *
 * @return A dictionary rappresenting the current row with column names as keys.
 *         When there are no more rows None will be returned
 */
static PyObject *pyhp_mysql_fetch(PyHP_MySQL_Result *self, PyObject *args)
{
    MYSQL_ROW row;
    MYSQL_FIELD *fields;
    unsigned int num_fields;
    unsigned int i;
    unsigned long *lengths;
    PyObject * pyrow = PyDict_New();

    if (!self->result) {
        self->result = mysql_use_result(self->connection);
    }

    if (!self->result) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    num_fields = mysql_num_fields(self->result);
    fields = mysql_fetch_fields(self->result);
    row = mysql_fetch_row(self->result);
    lengths = mysql_fetch_lengths(self->result);

    if (!row) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    for(i = 0; i < num_fields; i++) {
        if (!row[i]) {
            PyDict_SetItemString(pyrow, fields[i].name, Py_None);
            continue;
        }

        pyhp_mysql_add_field(pyrow, row, fields, lengths, i);
    }

    return pyrow;
}

/**
 * Python member function of the Query Result object to return the number
 * of rows returned by the query
 *
 * @return The number of rows returned by the query or None.
 */
PyObject *pyhp_mysql_rcount(PyHP_MySQL_Result *self, PyObject *args)
{
    if (!self->result) {
        self->result = mysql_use_result(self->connection);
    }

    if (!self->result) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    {
        FILE *f = fopen("/tmp/log", "w");
        fprintf(f, "Rows: %d\n", mysql_num_rows(self->result));
        fclose(f);
    }
    return PyInt_FromLong(mysql_num_rows(self->result));
}

/**
 * Python member function of the Query Result object to return the number
 * of columns returned by the query
 *
 * @return The number of columns returned by the query or None.
 */
static PyObject *pyhp_mysql_ccount(PyHP_MySQL_Result *self, PyObject *args)
{
    if (!self->result) {
        self->result = mysql_use_result(self->connection);
    }

    if (!self->result) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    return PyInt_FromLong(mysql_num_fields(self->result));
}

/**
 * Python member function of the Query Result object to reset the current cursor
 * this will make possible to switch between fetch and fetch_all requests
 * and to perform again the query.
 *
 * @return None
 */
static PyObject *pyhp_mysql_closecur(PyHP_MySQL_Result *self, PyObject *args)
{
    if (self->result) {
        mysql_free_result(self->result);
        self->result = NULL;
    }

    Py_INCREF(Py_None);
    return Py_None;
}

static PyObject *pyhp_mysql_next(PyHP_MySQL_Result *self, PyObject *args)
{
    PyObject * ret = pyhp_mysql_fetch(self, args);

    if (ret != Py_None)
        return ret;
    else {
        PyErr_SetNone(PyExc_StopIteration);
        return NULL;
    }
}

static PyMethodDef PyHP_MySQL_Result_methods[] = {
    {"fetch_all", (PyCFunction)pyhp_mysql_fetch_all, METH_NOARGS, PyDoc_STR("Fetches all the results")},
    {"fetch", (PyCFunction)pyhp_mysql_fetch, METH_NOARGS, PyDoc_STR("Fecthes one row using a cursor")},
    {"next", (PyCFunction)pyhp_mysql_next, METH_NOARGS, PyDoc_STR("Fecthes next row using a cursor")},
    {"columnCount", (PyCFunction)pyhp_mysql_ccount, METH_NOARGS, PyDoc_STR("Number of columns for the fetched query")},
    {"closeCursor", (PyCFunction)pyhp_mysql_closecur, METH_NOARGS, PyDoc_STR("Reset cursor to perform again the query")},
    {"rowCount", (PyCFunction)pyhp_mysql_rcount, METH_NOARGS, PyDoc_STR("Number of rows for the fetched query")},
    {NULL,	NULL},
};

static PyTypeObject PyHP_MySQL_Result_Type = {
    PyObject_HEAD_INIT(NULL)
    0,                       /* ob_size */
    "pyhp.db.MySQLResult",         /* tp_name */
    sizeof(PyHP_MySQL_Result),          /* tp_basicsize */
    0,                       /* tp_itemsize */
    (destructor)PyHP_MySQL_Result_dealloc,                       /* tp_dealloc */
    0,                       /* tp_print */
    0,                       /* tp_getattr */
    0,                       /* tp_setattr */
    0,                       /* tp_compare */
    (reprfunc)PyHP_MySQL_Result_str,                       /* tp_repr */
    0,                       /* tp_as_number */
    0,                       /* tp_as_sequence */
    0,                       /* tp_as_mapping */
    0,                       /* tp_hash */
    0,                       /* tp_call */
    (reprfunc)PyHP_MySQL_Result_str,                       /* tp_str */
    0,                       /* tp_getattro */
    0,                       /* tp_setattro */
    0,                       /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    "PyHP MySQL Query Result",                       /* tp_doc */
    0,                       /* tp_traverse */
    0,                       /* tp_clear */
    0,                       /* tp_richcompare */
    0,                       /* tp_weaklistoffset */
    (getiterfunc)PyHP_MySQL_Result_iter,                       /* tp_iter */
    0,                       /* tp_iternext */
    PyHP_MySQL_Result_methods,          /* tp_methods */
    0,                       /* tp_members */
    0,                       /* tp_getset */
    0,                       /* tp_base */
    0,                       /* tp_dict */
    0,                       /* tp_descr_get */
    0,                       /* tp_descr_set */
    0,                       /* tp_dictoffset */
    0,   /* tp_init */
    0,                       /* tp_alloc */
    (newfunc)PyHP_MySQL_Result_new,                       /* tp_new */
};

/**
 * connect implementation for the MySQL backend.
 *
 * @param where The URL of the db in the format "mysql:host=where.net;dbname=db;port=12345"
 *        the port parameter is optional and also the dbname.
 * @param user The username to use to connect to the db
 * @param pass the password of the user
 *
 * @return a pointer to the mysql connection object
 */
PyObject * pyhp_mysql_connect(char *where, char const *user, char const *pass)
{
    MYSQL *mysql = mysql_init(NULL);
    char *host = strcasestr(where, "host=");
    char *db = strcasestr(where, "dbname=");
    char *port = strcasestr(where, "port=");
    int real_port = 0;

    if (host) {
        host += HOST_ASSIGN_LEN;
        char *p = strcasestr(host, ";");

        if (p)
            *p = '\0';
    }

    if (db) {
        db += DBNAME_ASSIGN_LEN;
        char *p = strcasestr(db, ";");

        if (p)
            *p = '\0';
    }

    if (port)
        real_port = atoi( port+PORT_ASSIGN_LEN );

    if (PyType_Ready(&PyHP_MySQL_Result_Type) < 0)
        return Py_None;

    if (mysql_real_connect(mysql, host, user, pass, db, real_port, NULL, 0) == mysql)
        return PyCObject_FromVoidPtr(mysql, NULL);
    else {
        Py_INCREF(Py_None);
        return Py_None;
    }
}

/**
 * Executes a SQL query on the db.
 *
 * @param mysql The pointer to the mysql connection
 * @param statement The Python string with the statement to execute
 *
 * @return the number of modified rows if the statement was successfull
 * or None on failure
*/
PyObject *pyhp_mysql_exec(void *mysql, PyObject *statement)
{
    MYSQL *connection = mysql;
    long r = mysql_real_query(connection, PyString_AsString(statement), PyString_Size(statement));

    if (!r) {
        r = mysql_affected_rows(connection);
        return PyInt_FromLong(r);
    }
    else {
        Py_INCREF(Py_None);
        return Py_None;
    }
}

/**
 * Executes a SQL query on the db.
 *
 * @param mysql The pointer to the mysql connection
 * @param statement The Python string with the statement to execute
 *
 * @return a new MySQL Query Result instance or None
 */
PyObject *pyhp_mysql_query(void *mysql, PyObject *statement)
{
    MYSQL *connection = mysql;
    long r = mysql_real_query(connection, PyString_AsString(statement), PyString_Size(statement));

    if (!r) {
        return (PyObject*)PyHP_MySQL_Result_new(&PyHP_MySQL_Result_Type, connection);
    }
    else {
        Py_INCREF(Py_None);
        return Py_None;
    }
}

/**
 * Closes a MySQL connection
 *
 * @param mysql The connection to close
 */
void pyhp_mysql_close(void *mysql)
{
    MYSQL *connection = mysql;
    mysql_close(connection);
}
